예외는 말 그대로 예외상황에서만 써야 한다. 프로그램 흐름을 예외로 제어하려 하면 안 된다.
API설계 시 프로그램 흐름을 제어할 때 예외를 쓸 수 밖에 없도록 만들지 않는다.
try {
int i = 0;
while(true)
a[i++].f();
} catch(ArrayIndexOutOfBoundsException e) {
}
이렇게 바꿔 써라
for (int i = 0; i < a.length; i++)
a[i].f();
복구 가능한 상황이라면 처리해야 하는 예외를 쓰고 프로그래밍 오류가 발생한 상황이라면 런타임 예외를 써라.
만약, 판단하기 힘들다면 처리하지 않는 예외를 던지는 것이 더 좋다(항목 41)
처리해야 하는 예외를 던지는 메소드를 호출하는 메소드는 catch 블록을 써서 이 예외를 잡거나,
같은 예외를 던진다고 선언하여 이 예외를 외부로 전파해야 한다.
프로그래머가 예외를 적절하게 처리할 수 없을 때는 처리하는 않는 예외를 던지는 것이 좋다
} catch(TheCheckedException e) {
throw new Error("Assertion error"); // 절대 일어나서는 안 되는 일
}
} catch(TheCheckedException e) {
e.printStackTrace();
System.exit(1); // 헉, 졌다
}
// 처리해야 하는 예외
try {
obj.action(args);
} catch(TheCheckedException e) {
// 예외상황을 처리한다
...
}
이렇게 바꾼다.
// 상태 검사 메소드와 처리하는 않는 예외
if (obj.actionPermitted(args)) {
obj.action(args);
} else {
// 예외상황을 처리한다.
...
}
실패하더라도 스레드가 끝나 버리면 그만이라고 판단하다면 다음과 같이 더 간단하게 만들 수 있다
obj.action(args);
예외가 발생하는 상황인지 확인하는 메소드와 실제 작업을 처리하는 메소드로 나눈 것은 "상태 검사 메소드"(항목 39)
와 같은 방식이다.
주의할 점
외부에서 동기화하지 ?고 동시에 이 메소드가 호출되는 객체를 쓰거나, 내부상태를 바꿀 수 있다면
메소드를 2개로 나누지 말아야 한다.
actionPermitted 메소드를 호출한 시점과 action메소드를 호출한 시점 사이에 객체의 상태가 바뀔 수 있기 때문이다.
코드41.1 Main.java 처리해야 하는 예외를 상태 검사 메소드와 처리하지 않는 예외로 바꾼다
public class Main41 {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
Main41 obj = new Main41();
String test = new String("Hello");
if (obj.actionPermitted(test)) {
obj.action(args);
} else {
System.out.println("Action not permitted");
}
}
public void action(String[] args) {
System.out.println("action.permitted");
}
boolean actionPermitted(Object obj) {
if (obj instanceof Main41)
return true;
return false;
}
}
자바 플랫폼 라이브러리는 대부분 API에서 쓸 수 있도록 처리하지 않는 예외의 기본형을 거의 다 제공한다.
이런 예외들을 살펴본다.
public abstract class LogComponent {
public abstract String getName();
public abstract Integer getCount();
public LogComponent add(LogComponent logComponent) throws UnsupportedOperationException {
throw new UnsupportedOperationException();
}
...
}
public class LogNode extends LogComponent {
private static final String DELI = "/";
private String name;
private List<LogComponent> logList = new ArrayList<LogComponent>();
public LogNode(String name) {
this.name = name;
}
public LogComponent add(LogComponent logComponent) {
logList.add(logComponent);
return this;
}
...
}
public class LogLeaf extends LogComponent {
private static final String DELI = "/";
private String name;
private Integer count;
public LogLeaf(String name, Integer count) {
this.name = name;
this.count = count;
}
@Override
public Integer getCount() {
return count;
}
...
}
높은 계층에서 낮은 계층의 예외를 잡아서 높은 계층의 추상화 수준에 맞게 변환해서 던져야 한다. - 예외 변환이라고 한다.
남용하지 않게 조심해야 한다.
try {
// 낮은 계층의 추상화를 써서 작업을 처리한다.
...
} catch(LowerLevelException e) {
throw HigherLevelException(...);
}
public Object get(int index) {
ListIterator i = listIterator(index);
try {
return i.next();
} catch(noSuchElementException e) {
throw new IndexOutOfBoundsException("Index: " + index);
}
public void batchInsertRefererKeyword(final String period, final List<RefererKeywordSet> refererKeywordSets)
throws DataAccessException {
getBlogstatSqlMapClientTemplate().execute(new SqlMapClientCallback() {
public Object doInSqlMapClient(SqlMapExecutor executor)
throws SQLException {
executor.startBatch();
for (int i = 0; i < refererKeywordSets.size(); i++) {
try {
insertRefererKeyword(period, refererKeywordSets.get(i));
} catch (Exception e) {
RefererKeywordSet keyword = refererKeywordSets.get(i);
throw new LoadException(keyword.toString(), e);
}
}
@SuppressWarnings("unused")
int insCount = executor.executeBatch();
return null;
}
});
}
각 메소드가 던지는 모든 예외를 신중하게 문서화해야 한다.
/**
* IndexOutOfBoundsException 을 생성한다.
* @param lowerBound 정당한 최하위 인덱스 값
* @param upperBound 정당한 최상위 인덱스 값에 1을 더한 값
* @param index 실제 인덱스 값
*/
public IndexOutOfBoundsException(int lowerBound, int upperBound,
int index) {
// 실패를 자세히 설명할 수 있는 세부 메시지를 만들어라.
supper( "Lower bound: " + lowerBound +
", Upper bound: " + upperBound +
", Index: " + index);
}
예외를 던지고 난 객체는 어떤 상태여야 할까? 객체상태는 분명하고 다시 쓸 수 있는 것이 바람직하다.
메소드 호출이 실패하더라도 객체상태는 메소드 호출 전과 같아야 한다
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = element[--size];
element[size] = null; // 쓸모 없는 참조 제거
return result;
가령, 메소드 호출을 try 블록으로 둘러싸고는 빈 catch 블록을 써서 예외를 무시하는 경우가 많은데
의심해 볼 만하다.
예외를 잡아서 버리면 프로그램은 오류가 발생해도 아무 일 없듯이 잘 진행되는 것 처럼 보인다.
catch 블록 안에서 정말 아무것도 할 것이 없다면 최소한 왜 예외를 잡아서 처리하지 않고
버리는지 그 이유라도 주석으로 달아 놓아야 한다.